package net.oschina.bilbodai.common.beanutil.bean.code;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import net.oschina.bilbodai.common.beanutil.bean.AttrPathQueryInfo;
import net.oschina.bilbodai.common.beanutil.bean.PropertyDumpType;
import net.oschina.bilbodai.common.beanutil.bean.core.PropertyDumpInfo;
import net.oschina.bilbodai.common.beanutil.bean.core.PropertyInfo;
import net.oschina.bilbodai.common.beanutil.bean.creator.CollectToCollectCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.CollectToMapCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.EnumToStringCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.IPropertyCodeApplierCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.ITCollectToITMapCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.ITMapToITCollectCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.ITMapToITMapCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.ItCollectToItCollectCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.MapToCollectCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.MapToMapCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.PrimeToReferenceCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.ReferenceToPrimeCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.SimpleToSimpleCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.SingleToSingleCreator;
import net.oschina.bilbodai.common.beanutil.bean.creator.StringToEnumCreator;
import net.oschina.bilbodai.common.beanutil.exception.PropertyDumpException;
import net.oschina.bilbodai.common.beanutil.util.TypeUtil;

import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * @author daimao  Date: 16/5/28 Time: 下午4:58
 * @version $Id$
 */
public class PropertyCodeAppliers {

    private static Table<PropertyDumpType, PropertyDumpType, IPropertyCodeApplierCreator> codeApplierTable = HashBasedTable.create();

    static {
        //        codeApplierTable.put(PropertyDumpType.NONE, PropertyDumpType.NONE, SimpleToSimpleCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.REFERENCE, PropertyDumpType.PRIME, ReferenceToPrimeCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.PRIME, PropertyDumpType.REFERENCE, PrimeToReferenceCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.STRING, PropertyDumpType.ENUM, StringToEnumCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.ENUM, PropertyDumpType.STRING, EnumToStringCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.REFERENCE, PropertyDumpType.REFERENCE, SimpleToSimpleCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.REFERENCE, PropertyDumpType.NONE, SimpleToSimpleCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.NONE, PropertyDumpType.REFERENCE, SimpleToSimpleCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.IT_LIST, PropertyDumpType.IT_LIST, ItCollectToItCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.NONE, PropertyDumpType.IT_LIST, ItCollectToItCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.IT_LIST, PropertyDumpType.NONE, ItCollectToItCollectCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.IT_SET, PropertyDumpType.IT_SET, ItCollectToItCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.NONE, PropertyDumpType.IT_SET, ItCollectToItCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.IT_SET, PropertyDumpType.NONE, ItCollectToItCollectCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.IT_LIST, PropertyDumpType.IT_SET, ItCollectToItCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.IT_LIST, PropertyDumpType.IT_SET, ItCollectToItCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.IT_SET, PropertyDumpType.IT_LIST, ItCollectToItCollectCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.SINGLE, PropertyDumpType.SINGLE, SingleToSingleCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.NONE, PropertyDumpType.SINGLE, SingleToSingleCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.SINGLE, PropertyDumpType.NONE, SingleToSingleCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.LIST, PropertyDumpType.LIST, CollectToCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.LIST, PropertyDumpType.NONE, CollectToCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.NONE, PropertyDumpType.LIST, CollectToCollectCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.SET, PropertyDumpType.SET, CollectToCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.SET, PropertyDumpType.LIST, CollectToCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.LIST, PropertyDumpType.SET, CollectToCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.SET, PropertyDumpType.NONE, CollectToCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.NONE, PropertyDumpType.SET, CollectToCollectCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.MAP, PropertyDumpType.MAP, MapToMapCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.IT_MAP, PropertyDumpType.IT_MAP, ITMapToITMapCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.MAP, PropertyDumpType.LIST, MapToCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.MAP, PropertyDumpType.SET, MapToCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.LIST, PropertyDumpType.MAP, CollectToMapCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.SET, PropertyDumpType.MAP, CollectToMapCreator.INSTANCE);

        codeApplierTable.put(PropertyDumpType.IT_LIST, PropertyDumpType.IT_MAP, ITCollectToITMapCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.IT_SET, PropertyDumpType.IT_MAP, ITCollectToITMapCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.IT_MAP, PropertyDumpType.IT_LIST, ITMapToITCollectCreator.INSTANCE);
        codeApplierTable.put(PropertyDumpType.IT_MAP, PropertyDumpType.IT_SET, ITMapToITCollectCreator.INSTANCE);
    }

    public static IPropertyCodeApplier getCodeAppliers(
            PropertyDumpInfo propertyDumpInfo,
            PropertyInfo sourceInfo,
            PropertyInfo targetInfo,
            PropertyDumpType srcDumpType,
            PropertyDumpType targetDumpType
    ) {

        if (srcDumpType.isNotSet() && targetDumpType.isNotSet()) {
            DumpEntry autoDumpEntry = findAutoDumpEntry(
                    propertyDumpInfo.getSrcField().getType(),
                    propertyDumpInfo.getTargetField().getType(),
                    propertyDumpInfo.getSrcKeyType(),
                    propertyDumpInfo.getTargetKeyType(),
                    propertyDumpInfo.getSrcComponentType(),
                    propertyDumpInfo.getTargetComponentType(),
                    propertyDumpInfo.getAttrPathQueryInfo()
            );
            srcDumpType = autoDumpEntry.sourceType;
            targetDumpType = autoDumpEntry.targetType;
        }

        //如果两种类型都没设置,那么说明dump的两个字段都没加注解,那么默认使用SIMPLE
        IPropertyCodeApplierCreator codeApplierCreator = codeApplierTable.get(srcDumpType, targetDumpType);

        if (codeApplierCreator == null) {
            throw new PropertyDumpException(
                    "None Compatible Property SourceType=" + srcDumpType + " with TargetType=" + targetDumpType +
                            "  Combination Found Source=" + sourceInfo + ", Target=" + targetInfo);
        }

        return codeApplierCreator.create(propertyDumpInfo, sourceInfo, targetInfo);
    }

    private static DumpEntry findAutoDumpEntry(
            Class<?> srcFieldType,
            Class<?> targetFieldType,
            Class<?> srcKeyType,
            Class<?> targetKeyType,
            Class<?> srcValueType,
            Class<?> targetValueType,
            AttrPathQueryInfo attrPathQueryInfo
    ) {
        if (targetFieldType.isAssignableFrom(srcFieldType)) { // 如果源字段类型可以直接赋值给目标字段类型
            if (Collection.class.isAssignableFrom(targetFieldType)) { // 说明是集合
                if (targetValueType.isAssignableFrom(srcValueType)) { //元素类型也兼容,那么采用直接赋值
                    return new DumpEntry(PropertyDumpType.REFERENCE, PropertyDumpType.REFERENCE);
                }
                if (srcValueType.getPackage().getName().startsWith("java") || targetValueType.getPackage().getName().startsWith("java")) {
                    return new DumpEntry(List.class.isAssignableFrom(srcFieldType) ? PropertyDumpType.IT_LIST : PropertyDumpType.IT_SET,
                            List.class.isAssignableFrom(targetFieldType) ? PropertyDumpType.IT_LIST : PropertyDumpType.IT_SET);
                }
                return new DumpEntry(List.class.isAssignableFrom(srcFieldType) ? PropertyDumpType.LIST : PropertyDumpType.SET,
                        List.class.isAssignableFrom(targetFieldType) ? PropertyDumpType.LIST : PropertyDumpType.SET);
            }
            if (Map.class.isAssignableFrom(targetFieldType)) {
                if (!targetKeyType.isAssignableFrom(srcKeyType)) {
                    throw new PropertyDumpException("Using Map to dump field, source Map KeyType should assigned to target Map KeyType");
                }
                if (targetValueType.isAssignableFrom(srcValueType)) { //键值类型都相同
                    return new DumpEntry(PropertyDumpType.REFERENCE, PropertyDumpType.REFERENCE);
                }
                return new DumpEntry(PropertyDumpType.MAP, PropertyDumpType.MAP);
            }
            //那就采用普通的赋值
            return new DumpEntry(PropertyDumpType.REFERENCE, PropertyDumpType.REFERENCE);
        }

        //如果源字段类型不可以直接赋值给目标字段类型
        if (Collection.class.isAssignableFrom(targetFieldType)) { // 说明是集合
            if (Collection.class.isAssignableFrom(srcFieldType)) {
                if (targetValueType.isAssignableFrom(srcValueType)) {
                    return new DumpEntry(List.class.isAssignableFrom(srcFieldType) ? PropertyDumpType.IT_LIST : PropertyDumpType.IT_SET,
                            List.class.isAssignableFrom(targetFieldType) ? PropertyDumpType.IT_LIST : PropertyDumpType.IT_SET);
                }
                if (srcValueType.getPackage().getName().startsWith("java") || targetValueType.getPackage().getName().startsWith("java")) {
                    return new DumpEntry(List.class.isAssignableFrom(srcFieldType) ? PropertyDumpType.IT_LIST : PropertyDumpType.IT_SET,
                            List.class.isAssignableFrom(targetFieldType) ? PropertyDumpType.IT_LIST : PropertyDumpType.IT_SET);
                }

                return new DumpEntry(List.class.isAssignableFrom(srcFieldType) ? PropertyDumpType.LIST : PropertyDumpType.SET,
                        List.class.isAssignableFrom(targetFieldType) ? PropertyDumpType.LIST : PropertyDumpType.SET);
            } else if (Map.class.isAssignableFrom(srcFieldType)) {
                if (!targetValueType.isAssignableFrom(srcValueType)) { //map转Collect,但是map的value类型和Collect的元素类型不一致
                    return new DumpEntry(PropertyDumpType.MAP, List.class.isAssignableFrom(targetFieldType) ? PropertyDumpType.LIST : PropertyDumpType.SET);
                } else {
                    return new DumpEntry(PropertyDumpType.IT_MAP, List.class.isAssignableFrom(targetFieldType) ? PropertyDumpType.IT_LIST : PropertyDumpType.IT_SET);
                }
            }
            throw new PropertyDumpException("Not Support now!");
        }
        if (Map.class.isAssignableFrom(targetFieldType)) {
            if (Map.class.isAssignableFrom(srcFieldType)) {
                if (!targetKeyType.isAssignableFrom(srcKeyType)) {
                    throw new PropertyDumpException("Using Map to dump field, source Map KeyType should assigned to target Map KeyType");
                }
                if (targetValueType.isAssignableFrom(srcValueType)) { //键值类型相同
                    return new DumpEntry(PropertyDumpType.IT_MAP, PropertyDumpType.IT_MAP);
                }
                return new DumpEntry(PropertyDumpType.MAP, PropertyDumpType.MAP);
            } else if (Collection.class.isAssignableFrom(srcFieldType)) {
                if (!targetValueType.isAssignableFrom(srcValueType)) {
                    return new DumpEntry(List.class.isAssignableFrom(srcFieldType) ? PropertyDumpType.LIST : PropertyDumpType.SET, PropertyDumpType.MAP);
                } else {
                    return new DumpEntry(List.class.isAssignableFrom(srcFieldType) ? PropertyDumpType.IT_LIST : PropertyDumpType.IT_SET, PropertyDumpType.IT_MAP);
                }
            }
            throw new PropertyDumpException("Not Support now!");
        }

        if (attrPathQueryInfo != null) {
            if (targetFieldType.isAssignableFrom(attrPathQueryInfo.getPropertyType())) {
                return new DumpEntry(PropertyDumpType.REFERENCE, PropertyDumpType.REFERENCE);
            }
        }

        if (srcFieldType.equals(String.class) && Enum.class.isAssignableFrom(targetFieldType)) {
            return new DumpEntry(PropertyDumpType.STRING, PropertyDumpType.ENUM);
        }

        if (Enum.class.isAssignableFrom(srcFieldType) && targetFieldType.equals(String.class)) {
            return new DumpEntry(PropertyDumpType.ENUM, PropertyDumpType.STRING);
        }

        //检查是否是原始类型和引用类型匹配
        if (TypeUtil.isRefToPrimitiveTypeMatch(srcFieldType, targetFieldType)) {
            return new DumpEntry(PropertyDumpType.REFERENCE, PropertyDumpType.PRIME);
        }
        if (TypeUtil.isPrimitiveToRefTypeMatch(srcFieldType, targetFieldType)) {
            return new DumpEntry(PropertyDumpType.PRIME, PropertyDumpType.REFERENCE);
        }

        //那就采用普通的赋值
        return new DumpEntry(PropertyDumpType.SINGLE, PropertyDumpType.SINGLE);

    }

    private static class DumpEntry {
        private final PropertyDumpType sourceType;
        private final PropertyDumpType targetType;

        public DumpEntry(PropertyDumpType sourceType, PropertyDumpType targetType) {
            this.sourceType = sourceType;
            this.targetType = targetType;
        }
    }
}